/*
 * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package org.eclipse.imagen;

import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Vector;
import org.eclipse.imagen.media.util.CaselessStringKeyHashtable;
import org.eclipse.imagen.media.util.PropertyUtil;
import org.eclipse.imagen.util.CaselessStringKey;

/**
 * A class that implements the <code>PropertySource</code> interface. Property names are treated in a case-insensitive
 * manner.
 *
 * @since JAI 1.1
 */
// In JAI 1.0.2 this class was namex org.eclipse.imagen.PropertySourceImpl
// and was package scope (as it is now).
class PropertyEnvironment implements PropertySource {

    /** The local PropertyGenerators of this PropertyEnvironment. */
    Vector pg;

    /** The sources of the associated node. The elements should be PropertySources. */
    Vector sources;

    // Dummy value to associate with a key in "suppressed"
    private static final Object PRESENT = new Object();

    /** The names of suppressed properties. */
    CaselessStringKeyHashtable suppressed;

    /**
     * Sources of properties of this node. The keys are property names and the values are Integers which should be
     * indexes into the Vector of sources.
     */
    CaselessStringKeyHashtable sourceForProp;

    /** The associated node, which is either a RenderedOp or a RenderableOp. */
    private Object op;

    /** Hash of property names. Values are either PropertyGenerators, PropertySources, or Integers. */
    private CaselessStringKeyHashtable propNames;

    /**
     * Locally added default PropertySource which will override the default property inheritance mechanism, viz.,
     * inheritance from the lowest indexed source if no other means was specified.
     */
    private PropertySource defaultPropertySource = null;

    /**
     * Flag which is set to indicate that the default PropertySource has not yet been mapped into the property name
     * table. Initially true as there is nothing to map.
     */
    private boolean areDefaultsMapped = true;

    /**
     * Constructs a <code>PropertyEnvironment</code>
     *
     * @param sources <code>PropertySource</code>s in operation source order.
     * @param generators <code>PropertyGenerator</code>s.
     * @param suppressed Names of suppressed properties.
     * @param sourceForProp Hash by property name of indexes of <code>PropertySource</code>s in <code>sources</code>
     *     from which to derive properties.
     * @param op The operation node.
     */
    public PropertyEnvironment(
            Vector sources, Vector generators, Vector suppressed, Hashtable sourceForProp, Object op) {
        this.sources = sources;
        this.pg = generators == null ? null : (Vector) generators.clone();

        // "suppressed" should never be null
        this.suppressed = new CaselessStringKeyHashtable();

        if (suppressed != null) {
            Enumeration e = suppressed.elements();

            while (e.hasMoreElements()) {
                this.suppressed.put(e.nextElement(), PRESENT);
            }
        }

        this.sourceForProp = (sourceForProp == null) ? null : new CaselessStringKeyHashtable(sourceForProp);

        this.op = op;

        hashNames();
    }

    /**
     * Returns an array of Strings recognized as names by this property source.
     *
     * @return an array of Strings giving the valid property names.
     */
    public String[] getPropertyNames() {
        mapDefaults();

        int count = 0;
        String names[] = new String[propNames.size()];
        for (Enumeration e = propNames.keys(); e.hasMoreElements(); ) {
            names[count++] = ((CaselessStringKey) e.nextElement()).getName();
        }

        return names;
    }

    /**
     * Returns an array of <code>String</code>s recognized as names by this property source that begin with the supplied
     * prefix. If no property names match, <code>null</code> will be returned. The comparison is done in a
     * case-independent manner.
     *
     * <p>The default implementation calls <code>getPropertyNames()</code> and searches the list of names for matches.
     *
     * @return an array of <code>String</code>s giving the valid property names.
     */
    public String[] getPropertyNames(String prefix) {
        // This gives us a list of all non-suppressed properties
        String[] propertyNames = getPropertyNames();
        return PropertyUtil.getPropertyNames(propertyNames, prefix);
    }

    /**
     * Returns the class expected to be returned by a request for the property with the specified name. If this
     * information is unavailable, <code>null</code> will be returned.
     *
     * <p>This implemention returns <code>null</code> to avoid provoking deferred calculations.
     *
     * @return The <code>Class</code> expected to be return by a request for the value of this property or <code>null
     *     </code>.
     */
    public Class getPropertyClass(String propertyName) {
        if (propertyName == null) {
            throw new IllegalArgumentException(JaiI18N.getString("Generic0"));
        }
        return null;
    }

    /**
     * Returns the value of a property.
     *
     * @param name the name of the property, as a String.
     * @return the value of the property, as an Object.
     */
    public Object getProperty(String name) {
        if (name == null) {
            throw new IllegalArgumentException(JaiI18N.getString("Generic0"));
        }

        mapDefaults();

        Object o = propNames.get(name);

        Object property = null;
        if (o == null) {
            return java.awt.Image.UndefinedProperty;
        } else if (o instanceof PropertyGenerator) {
            property = ((PropertyGenerator) o).getProperty(name, op);
        } else if (o instanceof Integer) { // copy from source
            int srcIndex = ((Integer) o).intValue();
            PropertySource src = (PropertySource) sources.elementAt(srcIndex);
            property = src.getProperty(name);
        } else if (o instanceof PropertySource) {
            property = ((PropertySource) o).getProperty(name);
        }

        return property;
    }

    /** ---- Methods to modify the local property environment. ---- */
    public void copyPropertyFromSource(String propertyName, int sourceIndex) {
        PropertySource propertySource = (PropertySource) sources.elementAt(sourceIndex);
        propNames.put(propertyName, propertySource);
        suppressed.remove(propertyName);
    }

    public void suppressProperty(String propertyName) {
        suppressed.put(propertyName, PRESENT);
        hashNames();
    }

    public void addPropertyGenerator(PropertyGenerator generator) {
        if (pg == null) {
            pg = new Vector();
        }
        pg.addElement(generator);

        // Remove suppressed status of any property being generated by
        // this PropertyGenerator
        removeSuppressedProps(generator);
        hashNames();
    }

    /**
     * Sets a PropertySource from which to derive all non-suppressed properties emitted by the PropertySource if and
     * only if neither a PropertyGenerator nor a copy-from-source directive exists for the requested property. This
     * PropertySource will supersede automatic inheritance from any (operation) sources. It should be used, for example,
     * when a rendered node wishes to derive property values from its rendering without overriding any user
     * configuration settings in the property environment of the node.
     */
    public void setDefaultPropertySource(PropertySource ps) {
        // If no change just return.
        if (ps == defaultPropertySource) {
            return;
        }

        if (defaultPropertySource != null) {
            // Return the table to zero state if defaults existed before.
            hashNames();
        }

        // Unset the flag.
        areDefaultsMapped = false;

        // Cache the parameter.
        defaultPropertySource = ps;
    }

    /**
     * Updates "propNames" hash with the default PropertySource. This method allows deferred access to the default
     * PropertySource so as to postpone calculations which access thereto might incur. Does nothing unless the default
     * PropertySource has changed.
     */
    private void mapDefaults() {
        if (!areDefaultsMapped) {
            // Set the flag.
            areDefaultsMapped = true;

            // Update with default PropertySource only if non-null.
            if (defaultPropertySource != null) {
                String[] names = defaultPropertySource.getPropertyNames();
                if (names != null) {
                    int length = names.length;
                    for (int i = 0; i < length; i++) {
                        if (!suppressed.containsKey(names[i])) {
                            Object o = propNames.get(names[i]);
                            if (o == null
                                    || // undefined property
                                    o instanceof Integer) { // default inheritance
                                // Add "defaultPropertySource" or
                                // replace default inheritance.
                                propNames.put(names[i], defaultPropertySource);
                            }
                        }
                    }
                }
            }
        }
    }

    private void removeSuppressedProps(PropertyGenerator generator) {
        String names[] = generator.getPropertyNames();
        for (int i = 0; i < names.length; i++) {
            suppressed.remove(names[i]);
        }
    }

    private void hashNames() {
        propNames = new CaselessStringKeyHashtable();

        // Copy properties from sources. This is the default behavior if no
        // other property source is specified. The sources are represented
        // via Integer values in propNames.
        if (sources != null) {
            // Then get property names from each source
            for (int i = sources.size() - 1; i >= 0; i--) {
                Object o = sources.elementAt(i);

                if (o instanceof PropertySource) {
                    PropertySource source = (PropertySource) o;
                    String[] propertyNames = source.getPropertyNames();

                    if (propertyNames != null) {
                        for (int j = 0; j < propertyNames.length; j++) {
                            String name = propertyNames[j];
                            if (!suppressed.containsKey(name)) {
                                propNames.put(name, new Integer(i));
                            }
                        }
                    }
                }
            }
        }

        // Get non-suppressed properties from PropertyGenerators.
        // The propNames values are PropertyGenerator instances.
        if (pg != null) {
            PropertyGenerator generator;

            for (Iterator it = pg.iterator(); it.hasNext(); ) {
                generator = (PropertyGenerator) it.next();
                if (generator.canGenerateProperties(op)) {
                    String[] propertyNames = generator.getPropertyNames();
                    if (propertyNames != null) {
                        for (int i = 0; i < propertyNames.length; i++) {
                            String name = propertyNames[i];
                            if (!suppressed.containsKey(name)) {
                                propNames.put(name, generator);
                            }
                        }
                    }
                }
            }
        }

        // Lastly, honor all the copyPropertyFromSource directives
        // The propNames values are PropertySource instances.
        if (sourceForProp != null) {
            for (Enumeration e = sourceForProp.keys(); e.hasMoreElements(); ) {
                CaselessStringKey name = (CaselessStringKey) e.nextElement();

                if (!suppressed.containsKey(name)) {
                    Integer i = (Integer) sourceForProp.get(name);
                    PropertySource propertySource = (PropertySource) sources.elementAt(i.intValue());
                    propNames.put(name, propertySource);
                }
            }
        }

        // Unset the default mapping flag.
        areDefaultsMapped = false;
    }
}
